package com.xzzz.expand.sentinel.metric.es.controller;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.xzzz.common.base.exception.XzException400;
import com.xzzz.common.base.pojo.R;
import com.xzzz.common.base.util.DateUtils;
import com.xzzz.expand.es.core.EsCurdManager;
import com.xzzz.expand.sentinel.metric.controller.AbstractSentinelController;
import com.xzzz.expand.sentinel.metric.pojo.MetricLineRes;
import com.xzzz.expand.sentinel.metric.pojo.MetricRes;
import com.xzzz.expand.sentinel.metric.pojo.ResourcesRes;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.*;
import org.elasticsearch.index.reindex.BulkByScrollResponse;
import org.elasticsearch.search.aggregations.*;
import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval;
import org.elasticsearch.search.aggregations.bucket.histogram.ExtendedBounds;
import org.elasticsearch.search.aggregations.bucket.histogram.Histogram;
import org.elasticsearch.search.aggregations.bucket.histogram.ParsedDateHistogram;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.metrics.ParsedStats;
import org.elasticsearch.search.aggregations.metrics.ParsedSum;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 流量监控【ES】
 *
 * @author xzzz
 */
@Slf4j
@RestController
@RequestMapping("/sentinel/es")
public class SentinelMetricControllerByElasticSearch extends AbstractSentinelController {

    @Autowired
    private EsCurdManager esManager;

    private static final String INDEX_NAME = "index_sentinel_metrics_log";

    /**
     * 一天内被请求的资源列表
     *
     * @param appName     应用名称，必填
     * @param notResource 排除的资源，某些系统资源不在列表中显示
     * @param startTime   开始时间
     * @param endTime     结束时间
     * @param interval    时间区间
     * @return 资源列表 {@link ResourcesRes}
     * @apiNote 按照请求数降序, 包含rt信息, success信息
     */
    @GetMapping("/resources")
    public R<List<ResourcesRes>> resources(String appName,
                                           @RequestParam(required = false) String notResource,
                                           @RequestParam(required = false) String startTime,
                                           @RequestParam(required = false) String endTime,
                                           @RequestParam(required = false) String interval) {
        if (StrUtil.isBlank(appName)) {
            throw new XzException400("应用名称不得为空，请选择应用");
        }

        String[] times = checkTime(startTime, endTime, interval);
        startTime = times[0];
        endTime = times[1];

        SearchSourceBuilder searchSource = new SearchSourceBuilder().timeout(TimeValue.timeValueSeconds(5)).size(0);
        BoolQueryBuilder bool = new BoolQueryBuilder();
        searchSource.query(bool);

        MatchQueryBuilder appNameMatch = QueryBuilders.matchQuery("log.file.path", appName).operator(Operator.AND);
        bool.must(appNameMatch);

        RangeQueryBuilder datetimeRange = QueryBuilders.rangeQuery("datetime")
                .gte(startTime).lte(endTime).format("yyyy-MM-dd HH:mm:ss");
        bool.must(datetimeRange);

        if (StrUtil.isNotBlank(notResource)) {
            TermQueryBuilder resourceTerm = QueryBuilders.termQuery("resource", notResource);
            bool.mustNot(resourceTerm);
        }

        // rt 用时指标聚合
        AggregationBuilder rtAggs = AggregationBuilders.stats("rtStats").field("rt");
        // success 指标聚合
        AggregationBuilder successAggs = AggregationBuilders.sum("successCount").field("success");
        // 资源聚合
        AggregationBuilder resourceAggs = AggregationBuilders
                .terms("resourceAggs")
                .field("resource.keyword")
                .size(100)
                .missing("未知资源")
                .order(BucketOrder.aggregation("successCount", false))
                .subAggregation(rtAggs).subAggregation(successAggs);
        searchSource.aggregation(resourceAggs);

        SearchResponse resp = esManager.search(INDEX_NAME, searchSource);

        List<ResourcesRes> resultList = new ArrayList<>();

        Aggregations aggregations = resp.getAggregations();
        if (aggregations == null) {
            return R.ok(resultList);
        }

        ParsedTerms resourceAggsRes = aggregations.get("resourceAggs");
        for (Terms.Bucket bucket : resourceAggsRes.getBuckets()) {
            ResourcesRes res = new ResourcesRes();
            res.setResource(bucket.getKeyAsString());
            res.setDocCount(bucket.getDocCount());

            ParsedStats stats = bucket.getAggregations().get("rtStats");
            res.setAvgRt((double) Math.round(stats.getAvg() * 100) / 100);
            res.setMinRt((int) stats.getMin());
            res.setMaxRt((int) stats.getMax());

            ParsedSum sum = bucket.getAggregations().get("successCount");
            res.setSuccess((int) sum.getValue());
            resultList.add(res);
        }

        return R.ok(resultList);
    }

    /**
     * 资源直方图
     *
     * @param appName            应用名称
     * @param resource           资源名称，该字段延续了 sentinel 原生API的资源命名
     * @param startTime          开始时间
     * @param endTime            结束时间
     * @param interval           时间区间
     * @param customInterval     自定义区间
     * @param customIntervalUnit 自定义区间单位
     * @return 资源直方图信息 {@link MetricLineRes}
     * @apiNote 包含日期区间内的 success,exception,pass,block,minRt,maxRt,avgRt
     */
    @GetMapping("/metric/line")
    public R<MetricLineRes> metricLine(@RequestParam(required = false) String appName,
                                       @RequestParam final String resource,
                                       @RequestParam(required = false) String startTime,
                                       @RequestParam(required = false) String endTime,
                                       @RequestParam(required = false) String interval,
                                       @RequestParam(required = false) String customIntervalUnit,
                                       @RequestParam(required = false) Integer customInterval) {
        // 根据时间区间自动计算开始和结束时间，区间的优先级更高
        String[] times = checkTime(startTime, endTime, interval);
        startTime = times[0];
        endTime = times[1];

        // 自定义区间
        DateHistogramInterval fixed_interval = null;
        if (StrUtil.isNotBlank(customIntervalUnit) && customInterval != null) {
            if ("seconds".equals(customIntervalUnit)) {
                fixed_interval = DateHistogramInterval.seconds(customInterval);
            } else if ("minutes".equals(customIntervalUnit)) {
                fixed_interval = DateHistogramInterval.minutes(customInterval);
            } else if ("hours".equals(customIntervalUnit)) {
                fixed_interval = DateHistogramInterval.hours(customInterval);
            } else if ("days".equals(customIntervalUnit)) {
                fixed_interval = DateHistogramInterval.days(customInterval);
            }
        } else {
            // 不使用自定义区间则使用时间区间
            fixed_interval = interval(startTime, endTime);
        }

        ResourceHistogram rh = explain(startTime, endTime, interval, null, null, "elasticsearch");

        SearchSourceBuilder searchSource = new SearchSourceBuilder().timeout(TimeValue.timeValueSeconds(5)).size(0);
        BoolQueryBuilder bool = new BoolQueryBuilder();
        searchSource.query(bool);
        // 资源名
        TermQueryBuilder resourceTerm = QueryBuilders.termQuery("resource.keyword", resource);
        bool.must(resourceTerm);
        // 应用
        if (StrUtil.isNotBlank(appName)) {
            MatchQueryBuilder appNameMatch = QueryBuilders.matchQuery("log.file.path", appName).operator(Operator.AND);
            bool.must(appNameMatch);
        }
        // 日期范围
        RangeQueryBuilder datetimeRange = QueryBuilders.rangeQuery("datetime")
                .gte(startTime).lte(endTime).format("yyyy-MM-dd HH:mm:ss");
        bool.must(datetimeRange);

        // 日期直方图聚合
        AggregationBuilder resourceAggs = AggregationBuilders
                .dateHistogram("datetimeHistogram")
                .field("datetime")
                .fixedInterval(fixed_interval)
                .format("yyyy-MM-dd HH:mm:ss")
                .extendedBounds(new ExtendedBounds(startTime, endTime));

        resourceAggs.subAggregation(AggregationBuilders.stats("aggsRt").field("rt"));
        resourceAggs.subAggregation(AggregationBuilders.sum("aggsPass").field("pass"));
        resourceAggs.subAggregation(AggregationBuilders.sum("aggsBlock").field("block"));
        resourceAggs.subAggregation(AggregationBuilders.sum("aggsException").field("exception"));
        resourceAggs.subAggregation(AggregationBuilders.sum("aggsSuccess").field("success"));

        searchSource.aggregation(resourceAggs);

        SearchResponse resp = esManager.search(INDEX_NAME, searchSource);
        Aggregations aggregations = resp.getAggregations();

        // 返回对象
        MetricLineRes metrics = new MetricLineRes();
        metrics.setResource(resource);
        metrics.setTitle(rh.getTitle());
        if (aggregations == null) {
            return R.ok(metrics);
        }

        ParsedDateHistogram dateHistogram = aggregations.get("datetimeHistogram");
        for (Histogram.Bucket bucket : dateHistogram.getBuckets()) {
            MetricRes metric = MetricRes.empty();
            metric.setDatetime(bucket.getKeyAsString());
            ParsedSum success = bucket.getAggregations().get("aggsSuccess");
            metric.setSuccess((int) success.getValue());

            ParsedSum exception = bucket.getAggregations().get("aggsException");
            metric.setException((int) exception.getValue());

            ParsedSum pass = bucket.getAggregations().get("aggsPass");
            metric.setPass((int) pass.getValue());

            ParsedSum block = bucket.getAggregations().get("aggsBlock");
            metric.setBlock((int) block.getValue());

            ParsedStats rt = bucket.getAggregations().get("aggsRt");
            metric.setMinRt((int) rt.getMin());
            metric.setMaxRt((int) rt.getMax());
            metric.setAvgRt((double) Math.round(rt.getAvg() * 100) / 100);
            metrics.addMetric(metric);
        }

        return R.ok(metrics);
    }


    /**
     * 集群过去12小时的总体信息
     *
     * @param appName 应用集群名称
     * @return 集群流量信息
     */
    @GetMapping("/metric/app")
    public R<MetricRes> metricApp(String appName) {
        if (StrUtil.isBlank(appName)) {
            throw new XzException400("应用名称不得为空，请选择应用");
        }

        String[] times = checkTime(-24);
        String startTime = times[0];
        String endTime = times[1];

        SearchSourceBuilder searchSource = new SearchSourceBuilder().timeout(TimeValue.timeValueSeconds(5)).size(0);
        BoolQueryBuilder bool = new BoolQueryBuilder();
        searchSource.query(bool);

        MatchQueryBuilder appNameMatch = QueryBuilders.matchQuery("log.file.path", appName).operator(Operator.AND);
        bool.must(appNameMatch);

        RangeQueryBuilder datetimeRange = QueryBuilders.rangeQuery("datetime")
                .gte(startTime).lte(endTime).format("yyyy-MM-dd HH:mm:ss");
        bool.must(datetimeRange);

        TermQueryBuilder resourceTerm = QueryBuilders.termQuery("resource", "__total_inbound_traffic__");
        bool.mustNot(resourceTerm);

        searchSource.aggregation(AggregationBuilders.stats("aggsRt").field("rt"));
        searchSource.aggregation(AggregationBuilders.sum("aggsPass").field("pass"));
        searchSource.aggregation(AggregationBuilders.sum("aggsBlock").field("block"));
        searchSource.aggregation(AggregationBuilders.stats("aggsSuccess").field("success"));
        searchSource.aggregation(AggregationBuilders.sum("aggsException").field("exception"));

        SearchResponse resp = esManager.search(INDEX_NAME, searchSource);
        Aggregations aggregations = resp.getAggregations();
        if (aggregations == null) {
            return R.ok(MetricRes.empty());
        }

        MetricRes metricRes = MetricRes.empty();
        ParsedStats aggsRt = aggregations.get("aggsRt");

        metricRes.setAvgRt((double) Math.round(aggsRt.getAvg() * 100) / 100);
        metricRes.setMinRt((int) aggsRt.getMin());
        metricRes.setMaxRt((int) aggsRt.getMax());

        ParsedSum aggsPass = aggregations.get("aggsPass");
        metricRes.setPass((int) aggsPass.getValue());

        ParsedSum aggsBlock = aggregations.get("aggsBlock");
        metricRes.setBlock((int) aggsBlock.getValue());

        ParsedStats aggsSuccess = aggregations.get("aggsSuccess");
        metricRes.setSuccess((int) aggsSuccess.getSum());
        metricRes.setMaxQps((int) aggsSuccess.getMax());
        metricRes.setAvgQps((int) aggsSuccess.getAvg());

        ParsedSum aggsException = aggregations.get("aggsException");
        metricRes.setException((int) aggsException.getValue());

        return R.ok(metricRes);
    }

    /**
     * 计算统计间隔, 统计间隔绝大多数情况下不允许自定义, 否则会造成数据过多, 如统计1天内的流量, 间隔为1S, 那么会产生86400条统计数据
     *
     * @param startTime 开始时间
     * @param endTime   结束时间
     * @return 根据时间计算统计区间，
     */
    private DateHistogramInterval interval(String startTime, String endTime) {
        long startStamp = DateUtils.toTimestamp(startTime);
        long endStamp = DateUtils.toTimestamp(endTime);
        long diff = (endStamp - startStamp) / 1000;
        if (diff <= 0) {
            throw new IllegalArgumentException("开始与结束时间错误");
        }

        // 小于10分钟, 按秒展示, 最多600条
        if (diff < 601) {
            return DateHistogramInterval.seconds(1);
        }
        // 小于2小时, 按10秒展示, 最多600条
        else if (diff < 7201) {
            return DateHistogramInterval.seconds(10);
        }
        // 小于10小时, 按分钟展示, 最多600条
        else if (diff < 36001) {
            return DateHistogramInterval.minutes(1);
        }
        // 小于二十四小时, 按2分钟展示, 最多720条
        else if (diff < 86401) {
            return DateHistogramInterval.minutes(2);
        }
        // 大于二十四小时, 按5分钟展示
        else {
            return DateHistogramInterval.minutes(5);
        }
    }


    /**
     * 指标过期
     */
    @Scheduled(cron = "0 0 0/2 * * ?")
    public void expire() {
        Date today = DateUtil.date();
        // 删除3天前的
        Date expireDate = DateUtil.offsetDay(today, -2);
        String expireDateStr = DateUtil.format(expireDate, "yyyy-MM-dd HH:mm:ss");
        QueryBuilder query = QueryBuilders.rangeQuery("datetime").lt(expireDateStr);
        BulkByScrollResponse deleteResponse = esManager.deleteByQuery(INDEX_NAME, query);
        log.warn("[SENTINEL] 删除小于 [{}] 的SENTINEL日志, 需要删除的条数: {}", expireDateStr, deleteResponse.getDeleted());
    }
}
